﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Microsoft.CSharp;
using CSW.Framework.Common;
using CSW.Framework.Common.Binding;
using CSW.Framework.Common.Binding.Interfaces;
using CSW.Framework.Common.Data;
using CSW.Framework.Linq.SchemaModel;

namespace CSW.Framework.Linq
{
    /// <summary>
    /// LINQ to SQL Helper
    /// </summary>
    public static class LinqToSqlHelper<TItem>
        where TItem : class
    {
        private const BindingFlags m_PublicInstanceBindingFlags = BindingFlags.Public | BindingFlags.Instance;
        private static readonly Dictionary<string, Type> m_TypeDictionary = new Dictionary<string, Type>();
        private static readonly object m_Locker = new object();

        /// <summary>
        /// Gets the wrapper type.
        /// </summary>
        /// <param name="propertyList">The property list.</param>
        /// <param name="where">The where clause. This parameter may be null.</param>
        /// <returns>The wrapper type.</returns>
        public static Type GetWrapperType(List<string> propertyList, IEnumerable<Where<TItem>> where)
        {
            lock (m_Locker)
            {
                Guard.ArgumentNotNull(propertyList, "propertyList");

                // Add primary keys
                Database schema = Database.GetSchema<TItem>();
                var tables = schema.Tables;
                string fullClassName = typeof(TItem).FullName;
                var table = tables.Where(p => p.FullClassName == fullClassName).Single();
                var primaryKeys = table.Properties.Where(p => p.Column != null && p.Column.IsKey);
                var primaryKeyList = primaryKeys.Select(p => p.PropertyName).ToArray();

                foreach (string primaryKey in primaryKeyList)
                {
                    if (!propertyList.Contains(primaryKey))
                        propertyList.Add(primaryKey);
                }

                // Build key
                StringBuilder fieldNameList = new StringBuilder();
                foreach (string column in propertyList)
                {
                    fieldNameList.AppendLine(column);
                }

                // See if the type is in the dictionary
                Type wrapperType;
                if (m_TypeDictionary.TryGetValue(fieldNameList.ToString(), out wrapperType))
                {
                    for (int i = 0; i < propertyList.Count; i++)
                    {
                        if (propertyList[i] == null)
                            continue;

                        if (propertyList[i].Contains("."))
                            propertyList[i] = "___" + propertyList[i].Replace(".", "_");
                    }
                    return wrapperType;
                }

                // Build type
                CodeDomProvider provider = new CSharpCodeProvider(new Dictionary<string, string> { { "CompilerVersion", "v3.5" } });

                CompilerParameters cp = new CompilerParameters();
                cp.ReferencedAssemblies.Add("System.dll");
                cp.ReferencedAssemblies.Add("System.Xml.dll");
                cp.ReferencedAssemblies.Add(typeof(Enumerable).Assembly.Location);
                cp.ReferencedAssemblies.Add("System.Data.dll");
                cp.ReferencedAssemblies.Add(typeof(IExtensionOf<>).Assembly.Location);
                cp.ReferencedAssemblies.Add(typeof(TItem).Assembly.Location);
                cp.ReferencedAssemblies.Add(typeof(BusinessObjectLinq<>).Assembly.Location);

                StringBuilder source = new StringBuilder();
                string newTypeName = typeof(TItem).Name + "Extension";

                string extensionOfInterfaceName = FixGenericName(typeof(IExtensionOf<>).FullName);

                source.AppendLine("using System.Linq;");
                source.AppendLine();
                source.AppendLine("public class " + newTypeName + " : " + extensionOfInterfaceName + "<" + fullClassName + ">, System.ComponentModel.INotifyPropertyChanged");
                source.AppendLine("{");
                source.AppendLine("    private " + fullClassName + " _original;");
                source.AppendLine("    private bool _isReady;");
                source.AppendLine();
                source.AppendLine("    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
                source.AppendLine();
                source.Append("    public " + newTypeName + "(" + fullClassName + " ___inItem___Item");

                List<string> constructorParameters = new List<string>();
                List<string> assignments = new List<string>();
                List<string> fields = new List<string>();
                for (int fieldIndex = 0; fieldIndex < propertyList.Count; fieldIndex++)
                {
                    string fieldName = propertyList[fieldIndex];

                    if (string.IsNullOrEmpty(fieldName))
                        continue;

                    string friendlyTypeName;
                    string newFieldName = fieldName;
                    if (fieldName.Contains("."))
                    {
                        string[] parts = fieldName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
                        Type currentType = typeof(TItem);
                        for (int i = 0; i < parts.Length; i++)
                        {
                            string part = parts[i];

                            PropertyInfo pi = currentType.GetProperty(part, m_PublicInstanceBindingFlags);
                            if (pi == null)
                                throw new InvalidOperationException(string.Format("'{0}' is not a valid path. Property '{1}' does not exist in type '{2}'.", fieldName, part, currentType.FullName));

                            currentType = pi.PropertyType;
                        }

                        if (GetConverter(currentType) == null)
                            continue;

                        friendlyTypeName = GetFriendlyTypeName(currentType);

                        newFieldName = "___" + fieldName.Replace(".", "_");
                    }
                    else
                    {
                        PropertyInfo pi = typeof(TItem).GetProperty(fieldName, m_PublicInstanceBindingFlags);
                        if (pi == null)
                            continue;

                        Type propertyType = pi.PropertyType;
                        friendlyTypeName = GetFriendlyTypeName(propertyType);
                    }

                    constructorParameters.Add(fieldName);
                    source.Append(string.Format(", {0} ___in{1}", friendlyTypeName, newFieldName));
                    assignments.Add(string.Format("        ___f{0} = ___in{0};", newFieldName));
                    fields.Add(string.Format("    private readonly {0} ___f{1};", friendlyTypeName, newFieldName));
                }
                source.AppendLine(")");
                source.AppendLine("    {");
                source.AppendLine("        _isReady = false;");
                source.AppendLine("        _original = ___inItem___Item;");
                source.AppendLine("        _original.PropertyChanged += OriginalPropertyChanged;");
                source.AppendLine();
                foreach (string assignment in assignments)
                    source.AppendLine(assignment);
                source.AppendLine("    }");
                source.AppendLine();

                source.AppendLine("    public " + newTypeName + "(" + fullClassName + " original)");
                source.AppendLine("    {");
                source.AppendLine("        _original = original;");
                source.AppendLine("        _original.PropertyChanged += OriginalPropertyChanged;");
                source.AppendLine("        _isReady = true;");
                source.AppendLine("    }");
                source.AppendLine();
                foreach (string field in fields)
                    source.AppendLine(field);
                source.AppendLine();

                for (int fieldIndex = 0; fieldIndex < propertyList.Count; fieldIndex++)
                {
                    string fieldName = propertyList[fieldIndex];

                    if (string.IsNullOrEmpty(fieldName))
                        continue;

                    if (fieldName.Contains("."))
                    {
                        string[] parts = fieldName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
                        Type currentType = typeof(TItem);
                        string ifStatement = string.Empty;
                        string aggregate = string.Empty;
                        for (int i = 0; i < parts.Length; i++)
                        {
                            string part = parts[i];
                            bool isLast = (i == parts.Length - 1);

                            if (!string.IsNullOrEmpty(aggregate))
                                aggregate += ".";
                            aggregate += part;

                            PropertyInfo pi = currentType.GetProperty(part, m_PublicInstanceBindingFlags);
                            if (pi == null)
                                throw new InvalidOperationException(string.Format("'{0}' is not a valid path. Property '{1}' does not exist in type '{2}'.", fieldName, part, currentType.FullName));

                            if (!isLast)
                            {
                                if (string.IsNullOrEmpty(ifStatement))
                                    ifStatement = "if (";
                                else
                                    ifStatement += " && ";

                                ifStatement += string.Format("_original.{0} != null", aggregate);
                            }

                            currentType = pi.PropertyType;
                        }

                        string friendlyTypeName = GetFriendlyTypeName(currentType);

                        if (GetConverter(currentType) == null)
                            continue;

                        string newFieldName = "___" + fieldName.Replace(".", "_");
                        propertyList[fieldIndex] = newFieldName;
                        string methodBody = "if (!_isReady) { return ___f" + newFieldName + "; } else { ";
                        if (!string.IsNullOrEmpty(ifStatement))
                            methodBody += ifStatement + ") { return _original." + fieldName + "; } else { return null; } ";
                        else
                            methodBody += string.Format("return _original.{0};", fieldName);

                        source.AppendLine(string.Format("    public {0} {1}", friendlyTypeName, newFieldName));
                        source.AppendLine("    {");
                        source.AppendLine("        get");
                        source.AppendLine("        {");
                        source.AppendLine("            " + methodBody + " }");
                        source.AppendLine("        }");
                        source.AppendLine("    }");
                        source.AppendLine();
                    }
                }

                foreach (string fieldName in propertyList)
                {
                    if (string.IsNullOrEmpty(fieldName))
                        continue;

                    if (!fieldName.Contains("."))
                    {
                        PropertyInfo pi = typeof(TItem).GetProperty(fieldName, m_PublicInstanceBindingFlags);
                        if (pi == null)
                            continue;

                        Type propertyType = pi.PropertyType;
                        string fullPropertyTypeName = GetFriendlyTypeName(propertyType);
                        
                        source.AppendLine(string.Format("    public {0} {1}", fullPropertyTypeName, fieldName));
                        source.AppendLine("    {");
                        source.AppendLine("        get { if (!_isReady) { return ___f" + fieldName + "; } else { return _original." + fieldName + "; } }");
                        if (pi.GetSetMethod(false) != null)
                            source.AppendLine("        set { if (!_isReady) { throw new System.InvalidOperationException(); } else { _original." + fieldName + " = value; } }");
                        source.AppendLine("    }");
                        source.AppendLine();
                    }
                }

                source.AppendLine("    public " + fullClassName + " GetObject()");
                source.AppendLine("    {");
                source.AppendLine("        _isReady = true;");
                source.AppendLine("        return _original;");
                source.AppendLine("    }");
                source.AppendLine();
                source.AppendLine("    public void OriginalPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)");
                source.AppendLine("    {");
                source.AppendLine("        System.ComponentModel.PropertyChangedEventHandler propertyChanged = PropertyChanged;");
                source.AppendLine("        if (propertyChanged != null)");
                source.AppendLine("            propertyChanged(sender, e);");
                source.AppendLine("    }");
                source.AppendLine();
                string linkedBindingListClassName = FixGenericName(typeof(LinkedBindingList<,>).FullName);
                source.AppendLine("    public static " + linkedBindingListClassName + "<" + newTypeName + ", " + fullClassName + "> GetEmptyBindingList()");
                source.AppendLine("    {");
                string cswDatabaseClassName = typeof(Database).FullName;
                source.AppendLine(string.Format("        return new {2}<{0}, {1}>(new {0}[]{{}}, new {1}[]{{}}, {3}.GetSchema<{1}>().Tables.Where(p => p.FullClassName == \"{1}\").Single().Properties.Where(p => p.Column != null && p.Column.IsKey).Select(p => p.PropertyName).ToArray());", newTypeName, fullClassName, linkedBindingListClassName, cswDatabaseClassName));
                //source.AppendLine("        return new " + linkedBindingListClassName + "<" + newTypeName + ", " + fullClassName + ">(new " + newTypeName + "[]{}, new " + fullClassName + "[]{});");
                source.AppendLine("    }");
                source.AppendLine();
                string whereClassName = FixGenericName(typeof(Where<>).FullName);
                source.AppendLine("    public static " + linkedBindingListClassName + "<" + newTypeName + ", " + fullClassName + "> GetNewBindingList(System.Collections.Generic.IEnumerable<" + whereClassName + "<" + fullClassName + ">> where)");
                source.AppendLine("    {");
                //source.AppendLine("        System.Console.WriteLine(\"func enter\");");
                string tableQueryProviderName = FixGenericName(typeof(TableQueryProvider<>).FullName);
                source.AppendLine("        System.Linq.IQueryable<" + fullClassName + "> x = new " + tableQueryProviderName + "<" + fullClassName + ">();");//.Select(p => p);");
                //source.AppendLine("        System.Console.WriteLine(\"got queryable\");");
                source.AppendLine();
                if (where != null && where.Any())
                {
                    string comparsionTypeEnumName = typeof(ComparisonType).FullName;

                    source.AppendLine("        " + whereClassName + "<" + fullClassName + "> whereItem;");
                    foreach (Where<TItem> whereItem in where)
                    {
                        if (whereItem.ComparisonType == ComparisonType.IsNull ||
                            whereItem.ComparisonType == ComparisonType.IsNotNull)
                        {
                            source.AppendLine(string.Format("        whereItem = where.Where(p => p.FieldName == \"{0}\" && p.Value == null && p.ComparisonType == {2}.{1}).SingleOrDefault();", whereItem.FieldName, whereItem.ComparisonType, comparsionTypeEnumName));
                        }
                        else
                        {
                            source.AppendLine(string.Format("        whereItem = where.Where(p => p.FieldName == \"{0}\" && p.Value != null && p.ComparisonType == {2}.{1}).SingleOrDefault();", whereItem.FieldName, whereItem.ComparisonType, comparsionTypeEnumName));
                        }

                        source.AppendLine("        if (whereItem != null)");
                        source.AppendLine("        {");

                        string prefix = string.Empty;
                        string suffix = string.Empty;
                        string op = string.Empty;
                        string castedValue = "(" + GetFriendlyTypeName(whereItem.ValueType) + ")value";

                        switch (whereItem.ComparisonType)
                        {
                            case ComparisonType.Contains:
                                prefix = ".Contains(";
                                suffix = ")";
                                break;

                            case ComparisonType.StartsWith:
                                prefix = ".StartsWith(";
                                suffix = ")";
                                break;

                            case ComparisonType.EndsWith:
                                prefix = ".EndsWith(";
                                suffix = ")";
                                break;

                            case ComparisonType.Equals:
                                op = "==";
                                break;

                            case ComparisonType.GreaterThan:
                                op = ">";
                                break;

                            case ComparisonType.GreaterThanOrEqual:
                                op = ">=";
                                break;

                            case ComparisonType.LessThan:
                                op = "<";
                                break;

                            case ComparisonType.LessThanOrEqual:
                                op = "<=";
                                break;

                            case ComparisonType.IsNull:
                                op = "==";
                                castedValue = "null";
                                break;

                            case ComparisonType.IsNotNull:
                                op = "!=";
                                castedValue = "null";
                                break;

                            default:
                                throw new NotSupportedException(string.Format("ComparisonType '{0}' not supported.", whereItem.ComparisonType));
                        }

                        source.AppendLine("            object value = whereItem.Value;");
                        source.AppendLine("            x = x.Where(p => p." + whereItem.FieldName + prefix + op + castedValue + suffix + ");");
                        source.AppendLine("        }");
                    }
                    source.AppendLine();
                }
                //source.AppendLine("        System.Console.WriteLine(\"built where\");");
                source.AppendLine("        System.Linq.IQueryable<" + newTypeName + "> y =");
                source.AppendLine("            x.Select(p => new " + newTypeName);
                source.AppendLine("            (");
                source.Append("                p");
                foreach (string constructorParam in constructorParameters)
                {
                    source.AppendLine(",");
                    source.Append(string.Format("                p.{0}", constructorParam));
                }
                source.AppendLine();
                source.AppendLine("            ));");
                //source.AppendLine("        System.Console.WriteLine(\"built second queryable\");");
                //source.AppendLine("        System.Diagnostics.Debug.Assert(y != null);");
                //source.AppendLine("        var z = y.Take(1000).ToList();");
                source.AppendLine("        var z = y.ToList();");
                //source.AppendLine("        System.Console.WriteLine(\"called tolist\");");
                //source.AppendLine("        System.Diagnostics.Debug.Assert(z != null);");
                source.AppendLine("        var a = z.Select(p => p._original).ToList();");
                //source.AppendLine("        System.Console.WriteLine(\"get original list\");");
                //source.AppendLine("        System.Diagnostics.Debug.Assert(a != null);");                
                string returnLine = string.Format("        return new {2}<{0}, {1}>(z, a, {3}.GetSchema<{1}>().Tables.Where(p => p.FullClassName == \"{1}\").Single().Properties.Where(p => p.Column != null && p.Column.IsKey).Select(p => p.PropertyName).ToArray());", newTypeName, fullClassName, linkedBindingListClassName, cswDatabaseClassName);
                source.AppendLine(returnLine);
                source.AppendLine("    }");
                source.AppendLine("}");

                // Compile
                cp.GenerateInMemory = true;
                //cp.OutputAssembly = string.Format("{0} ({1})", newTypeName, ++m_DynamicAssemblyCounter);
                cp.TreatWarningsAsErrors = true;
                cp.WarningLevel = 4;
                CompilerResults result = provider.CompileAssemblyFromSource(cp, source.ToString());
                if (result.Errors.HasErrors || result.Errors.HasWarnings)
                {
                    string errorText = string.Empty;
                    foreach (CompilerError error in result.Errors)
                        errorText += error.ErrorText;

                    throw new Exception(errorText);
                }

                // Get new type and add to dictionary
                Assembly newAssembly = result.CompiledAssembly;
                wrapperType = newAssembly.GetType(newTypeName);

                m_TypeDictionary.Add(fieldNameList.ToString(), wrapperType);

                return wrapperType;
            }
        }

        private static string FixGenericName(string p)
        {
            if (p[p.Length - 2] == '`')
                return p.Substring(0, p.Length - 2);
            else
                return p;
        }

        private static string GetConverter(Type type)
        {
            Guard.ArgumentNotNull(type, "type");

            string friendlyTypeName = GetFriendlyTypeName(type);
            bool isNullable = friendlyTypeName.StartsWith("System.Nullable<");
            string bareTypeName = friendlyTypeName;
            if (isNullable)
            {
                bareTypeName = friendlyTypeName.Remove(0, "System.Nullable<".Length).TrimEnd('>');
            }
            bareTypeName = bareTypeName.Replace("System.", "");

            string methodName = "Get" + (isNullable ? "Nullable" : string.Empty) + bareTypeName;

            if (typeof(DataConverter).GetMethod(methodName, BindingFlags.Static | BindingFlags.Public) == null)
                return null;

            string cswDataConverterName = typeof(DataConverter).FullName;
            return cswDataConverterName + "." + methodName;
        }

        private static string GetFriendlyTypeName(Type type)
        {
            if (type.IsGenericType)
            {
                string name = type.FullName.Split(new[] { '`' }, StringSplitOptions.RemoveEmptyEntries)[0];
                string typeParams = string.Empty;
                foreach (Type typeParameter in type.GetGenericArguments())
                {
                    if (!string.IsNullOrEmpty(typeParams))
                        typeParams += ", ";

                    typeParams += GetFriendlyTypeName(typeParameter);
                }
                return string.Format("{0}<{1}>", name, typeParams);
            }
            else
            {
                return type.FullName;
            }
        }
    }

    /// <summary>
    /// LINQ to SQL Helper
    /// </summary>
    public static class LinqToSqlHelper
    {
        /// <summary>
        /// Gets the name of the field from a field expression.
        /// </summary>
        /// <param name="field">Field expression.</param>
        /// <returns>The name of the field.</returns>
        public static string GetFieldName(LambdaExpression field)
        {
            Guard.ArgumentNotNull(field, "field");

            string fieldName = field.Body.ToString();
            fieldName = fieldName.Substring(fieldName.IndexOf('.') + 1);
            return fieldName;
        }

        internal static object GetMemberExpressionValue(MemberExpression m)
        {
            List<MemberExpression> memEx = new List<MemberExpression>();
            memEx.Add(m);
            while (m.Expression is MemberExpression)
            {
                m = (MemberExpression)m.Expression;
                memEx.Add(m);
            }
            memEx.RemoveAt(memEx.Count - 1);

            ConstantExpression c = (ConstantExpression)m.Expression;
            object value = c.Value;
            int idx;

            do
            {
                MemberInfo memberInfo = m.Member;
                if (memberInfo is FieldInfo)
                {
                    FieldInfo fieldInfo = (FieldInfo)memberInfo;
                    value = fieldInfo.GetValue(value);
                }
                else if (memberInfo is PropertyInfo)
                {
                    PropertyInfo propInfo = (PropertyInfo)memberInfo;
                    MethodInfo methodInfo = propInfo.GetGetMethod(true);
                    value = methodInfo.Invoke(value, null);
                }
                else
                {
                    throw new NotSupportedException(string.Format("MemberInfo of type '{0}' not supported.", memberInfo.GetType()));
                }

                idx = memEx.Count - 1;
                if (idx >= 0)
                {
                    m = memEx[idx];
                    memEx.RemoveAt(idx);
                }
            } while (idx >= 0);

            return value;
        }
    }
}